package com.heima.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.config.RabbitMQSend;
import com.heima.feign.ArticleFeignClient;
import com.heima.mapper.NewsMapper;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.PageResponseResult;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.constants.AliyunConstants;
import com.heima.model.constants.NewsConstants;
import com.heima.model.constants.RabbitMQConstants;
import com.heima.model.constants.WemediaConstants;
import com.heima.model.user.dtos.AuthDto;
import com.heima.model.wemedia.dtos.NewsAuthDto;
import com.heima.model.wemedia.dtos.WmNewsDto;
import com.heima.model.wemedia.dtos.WmNewsPageReqDto;
import com.heima.model.wemedia.pojos.*;
import com.heima.service.*;
import com.heima.utils.WMThreadLocal;
import com.heima.utils.common.AliyunDetect;
import com.heima.utils.common.AliyunOCR;
import com.heima.utils.common.SensitiveWordUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author chenshan
 */
@Service
public class NewsServiceImpl extends ServiceImpl<NewsMapper, WmNews> implements NewsService {
    /**
     * 分页条件查询文章列表
     *
     * @param pageReqDto 分页+条件
     * @return PageResponseResult
     */
    @Autowired
    private MaterialService materialService;

    @Autowired
    private NewsMaterialService newsMaterialService;

    @Autowired
    private WMUserService userService;

    @Autowired
    private ChannelService channelService;

    @Autowired
    private ArticleFeignClient articleFeignClient;

    @Autowired
    private SensitiveService sensitiveService;

    @Autowired
    private RabbitMQSend rabbitMQSend;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 自媒体文章的分页条件查询
     *
     * @param pageReqDto 条件
     * @return 结果
     */
    @Override
    public PageResponseResult newsList(WmNewsPageReqDto pageReqDto) {
        // 校验分页参数
        pageReqDto.checkParam();

        // 分页条件查询
        Page<WmNews> page = new Page<>(pageReqDto.getPage(), pageReqDto.getSize());
        page(page, Wrappers.lambdaQuery(WmNews.class)
                .eq(ObjectUtil.isNotEmpty(WMThreadLocal.get()), WmNews::getUserId, WMThreadLocal.get())
                .eq(ObjectUtil.isNotEmpty(pageReqDto.getStatus()), WmNews::getStatus, pageReqDto.getStatus())
                .like(ObjectUtil.isNotEmpty(pageReqDto.getKeyword()), WmNews::getTitle, pageReqDto.getKeyword())
                .eq(ObjectUtil.isNotEmpty(pageReqDto.getChannelId()), WmNews::getChannelId, pageReqDto.getChannelId())
                .between(ObjectUtil.isAllNotEmpty(pageReqDto.getBeginPubdate(), pageReqDto.getEndPubdate()), WmNews::getPublishTime, pageReqDto.getBeginPubdate(), pageReqDto.getEndPubdate())
                .orderByDesc(WmNews::getCreatedTime));

        PageResponseResult pageResponseResult = new PageResponseResult(Convert.toInt(page.getCurrent()), Convert.toInt(page.getSize()), Convert.toInt(page.getTotal()));
        pageResponseResult.setData(page.getRecords());
        pageResponseResult.setHost("");
        return pageResponseResult;
    }

    /**
     * 自媒体端文章的发布功能
     *
     * @param newsDto 文章信息
     * @return ResponseResult
     */
    @Override
    public ResponseResult newsSubmit(WmNewsDto newsDto, Boolean draft) throws Exception {
        // 校验参数 - 501 无效参数
        if (!ObjectUtil.isAllNotEmpty(newsDto.getChannelId(), newsDto.getContent(), newsDto.getLabels(), newsDto.getPublishTime(), newsDto.getStatus(), newsDto.getTitle(), newsDto.getType())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 拷贝参数值到WmNews对象中，并为对象绑定当前用户
        WmNews news = new WmNews();
        BeanUtil.copyProperties(newsDto, news, "images");

        // 单独拼接图片地址
        String images = StringUtils.join(newsDto.getImages(), ",");
        news.setImages(images);
        news.setUserId(WMThreadLocal.get());

        // 解析content内容，得到文章中的图片
        Map<String, Object> content = parseContent(news.getContent());
        ArrayList<String> contentImageUrls = (ArrayList<String>) content.get(WemediaConstants.WM_NEWS_TYPE_IMAGE);

        // 解析封面图片
        List<String> picImageUrls = newsDto.getImages();

        // 判断封面类型是否为自动（type=-1）=> 转换为三图/单图/无图
        if (news.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)) {
            if (contentImageUrls.size() >= WemediaConstants.WM_NEWS_MANY_IMAGE) {
                news.setType(WemediaConstants.WM_NEWS_MANY_IMAGE);
                List<String> collect = contentImageUrls.stream().limit(3).collect(Collectors.toList());
                picImageUrls.addAll(collect);
                images = StringUtils.join(collect, ",");
                news.setImages(StringUtils.join(collect, ","));
            } else if (contentImageUrls.size() >= WemediaConstants.WM_NEWS_SINGLE_IMAGE) {
                news.setType(WemediaConstants.WM_NEWS_SINGLE_IMAGE);
                List<String> collect = contentImageUrls.stream().limit(1).collect(Collectors.toList());
                picImageUrls.addAll(collect);
                images = StringUtils.join(collect, ",");
                news.setImages(StringUtils.join(collect, ","));
            } else {
                news.setType(WemediaConstants.WM_NEWS_NONE_IMAGE);
            }
        }

        // 设置封面图片
        news.setImages(images);

        // 根据素材表，将图片Urls转换为图片Ids
        List<Integer> contentImageIds = getMaterialIdsByUrls(contentImageUrls);
        List<Integer> picImageIds = getMaterialIdsByUrls((ArrayList<String>) picImageUrls);

        // 将文章信息保存到news表
        // 判断草稿是否存在 -- 修改草稿
        if (draft && ObjectUtil.isNotEmpty(news.getId())) {
            // 先删除中间表数据 -- 根据此篇文章的id删除与此篇文章有关的所有素材的联系
            newsMaterialService.remove(Wrappers.lambdaQuery(WmNewsMaterial.class)
                    .eq(WmNewsMaterial::getNewsId, news.getId()));

            // 草稿提交
            news.setSubmitedTime(new Date());
            updateById(news);
        } else {
            // 新建提交
            if (!draft) {
                news.setSubmitedTime(new Date());
            }
            news.setCreatedTime(new Date());
            save(news);
        }

        // 保存封面图片和新闻id到中间表中
        addNewsMaterialRelation(news.getId(), contentImageIds, WemediaConstants.WM_CONTENT_REFERENCE);
        addNewsMaterialRelation(news.getId(), picImageIds, WemediaConstants.WM_COVER_REFERENCE);

        // 提交审核并保存到数据库后，进行自动审核
        if (!draft) {
            // 文章审核
            newsContentDetect(news);

            // 文章审核(立即审核) MQ实现
            // rabbitMQSend.sendDelayMessage(news.getId(), RabbitMQConstants.RABBITMQ_RELEASE_KEY, 0L);
            // Thread.sleep(500L);
        }

        // 如果发布时间小于系统当前时间，调用自动发布
        if (news.getStatus().equals(WmNews.Status.SUCCESS.getCode())) {
            if (DateTime.now().isAfterOrEquals(news.getPublishTime())) {
                publishNewsToArticle(news);
            } else {
                // 进行延迟发布
                rabbitMQSend.sendDelayMessage(news.getId(), RabbitMQConstants.RABBITMQ_DELAY_KEY, DateUtil.between(news.getPublishTime(), new Date(), DateUnit.MS));
            }
        }

        return ResponseResult.okResult(null);
    }

    /**
     * 文章的上架或下架
     *
     * @param news 文章信息
     * @return ResponseResult
     */
    @Override
    public ResponseResult newsDownOrUp(WmNews news) {
        // 校验参数的合法性 文章 文章id 文章是否下架
        if (!ObjectUtil.isAllNotEmpty(news, news.getId(), news.getEnable())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 判断文章是否存在
        WmNews wmNews = getById(news.getId());
        if (ObjectUtil.isNull(wmNews)) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID, "对应的文章不存在~");
        }

        // 判断文章是否已发表（未发表不允许上下架操作）
        if (!wmNews.getStatus().equals(WmNews.Status.PUBLISHED.getCode())) {
            ResponseResult.errorResult(5000, "文章尚未发布~");
        }

        // 启用状态相同，不做修改
        if (news.getEnable().equals(wmNews.getEnable())) {
            ResponseResult.okResult(null);
        }

        // 修改文章的上架状态
        wmNews.setEnable(news.getEnable());
        updateById(wmNews);

        // 使用feign同步调用
        // articleFeignClient.articleConfigDownOrUp(wmNews);

        // 使用RabbitMQ异步调用
        rabbitTemplate.convertAndSend(RabbitMQConstants.RABBITMQ_ENABLE_QUEUE, wmNews);
        return ResponseResult.okResult(null);
    }

    /**
     * 查询自媒体文章列表
     *
     * @param authDto 自媒体文章信息
     * @return ResponseResult
     */
    @Override
    public ResponseResult findAllNews(NewsAuthDto authDto) {
        // 校验分页参数
        authDto.checkParam();

        // 开启分页
        Page<WmNews> page = new Page<>(authDto.getPage(), authDto.getSize());
        page(page, Wrappers.lambdaQuery(WmNews.class)
                .eq(ObjectUtil.isNotEmpty(authDto.getStatus()), WmNews::getStatus, authDto.getStatus())
                .like(ObjectUtil.isNotEmpty(authDto.getTitle()), WmNews::getTitle, authDto.getTitle()));

        PageResponseResult result = new PageResponseResult();
        result.setCurrentPage((int) page.getCurrent());
        result.setSize((int) page.getSize());
        result.setTotal((int) page.getTotal());
        result.setData(page.getRecords());
        result.setHost("");

        return result;
    }

    /**
     * 查询自媒体文章详情
     * @param id 自媒体文章id
     * @return ResponseResult
     */
    @Override
    public ResponseResult findNewById(Integer id) {
        // 校验参数的合法性
        if (ObjectUtil.isNull(id)) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        WmNews news = getById(id);
        return ResponseResult.okResult(news);
    }

    /**
     * 自媒体文章审核驳回
     * @param authDto 自媒体文章驳回信息
     * @return ResponseResult
     */
    @Override
    public ResponseResult authFailNews(NewsAuthDto authDto) {
        // 校验参数的合法性
        if (!ObjectUtil.isAllNotEmpty(authDto.getId(), authDto.getMsg(), authDto.getStatus())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 修改文章的状态
        WmNews news = new WmNews();
        news.setId(authDto.getId());
        news.setReason(authDto.getMsg());
        news.setStatus(authDto.getStatus());

        updateById(news);

        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

    /**
     * 自媒体文章审核通过
     * @param authDto 自媒体文章通过信息
     * @return ResponseResult
     */
    @Override
    public ResponseResult authPassNews(NewsAuthDto authDto) {
        // 校验参数的合法性
        if (!ObjectUtil.isAllNotEmpty(authDto.getId(), authDto.getStatus())) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }

        // 修改文章状态
        WmNews news = getById(authDto.getId());
        news.setStatus(authDto.getStatus());
        news.setReason("人工审核通过");
        updateById(news);

        // 人工审核通过后实现自动发布
        publishNewsToArticle(news);

        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

    /**
     * 本地自定义敏感词校验（DFA校验）
     * @param content 检测内容
     * @return ResponseResult<Boolean>
     */
    @Override
    public ResponseResult<Boolean> checkContent(String content) {
        Boolean handleSensitiveScan = handleSensitiveScan(content);
        return ResponseResult.okResult(handleSensitiveScan);
    }

    /**
     * 根据图片Url，从素材表中获取图片id
     *
     * @param urls 图片Url集
     * @return 图片id集
     */
    private List<Integer> getMaterialIdsByUrls(ArrayList<String> urls) {
        List<Integer> materialIds = new ArrayList<>();
        for (String url : urls) {
            WmMaterial material = materialService.getOne(Wrappers.lambdaQuery(WmMaterial.class)
                    .eq(WmMaterial::getUrl, url));
            if (ObjectUtil.isNotEmpty(material)) {
                materialIds.add(material.getId());
            }
        }

        return materialIds;
    }

    /**
     * 解析文章内容(图片和文本)
     *
     * @param s 文章内容
     * @return Map<String, Object>
     */
    private Map<String, Object> parseContent(String s) {
        List<String> contentList = new ArrayList<>();
        StringBuilder builder = new StringBuilder();
        List<Map> mapList = JSONUtil.toList(s, Map.class);
        for (Map map : mapList) {
            if (StrUtil.equals(Convert.toStr(map.get(NewsConstants.WM_NEWS_CONTENT_TYPE)), WemediaConstants.WM_NEWS_TYPE_TEXT)) {
                builder.append(Convert.toStr(map.get(NewsConstants.WM_NEWS_CONTENT_VALUE)));
            }
            if (StrUtil.equals(Convert.toStr(map.get(NewsConstants.WM_NEWS_CONTENT_TYPE)), WemediaConstants.WM_NEWS_TYPE_IMAGE)) {
                contentList.add(Convert.toStr(map.get(NewsConstants.WM_NEWS_CONTENT_VALUE)));
            }
        }

        Map<String, Object> map = new HashMap<>();
        map.put(WemediaConstants.WM_NEWS_TYPE_TEXT, builder.toString());
        map.put(WemediaConstants.WM_NEWS_TYPE_IMAGE, contentList);

        return map;
    }

    /**
     * 保存封面图片和文章中图片到中间表
     *
     * @param newsId   文章id
     * @param imageIds 图片ids
     * @param type     引用类型
     */
    private void addNewsMaterialRelation(Integer newsId, List<Integer> imageIds, Short type) {
        for (Integer imageId : imageIds) {
            WmNewsMaterial newsMaterial = new WmNewsMaterial();
            newsMaterial.setMaterialId(imageId);
            newsMaterial.setNewsId(newsId);
            newsMaterial.setType(type);
            newsMaterial.setOrd((short) 0);
            newsMaterialService.save(newsMaterial);
        }
    }

    /**
     * 文章审核
     *
     * @param news 文章
     */
    @Override
    public void newsContentDetect(WmNews news) throws Exception {
        // 解析Content
        Map<String, Object> map = parseContent(news.getContent());
        // 获取文章的文本 内容 标题 标签
        String text = map.get(WemediaConstants.WM_NEWS_TYPE_TEXT) + news.getTitle() + news.getLabels();

        // 获取文章的图片集合
        List<String> imageUrl = (List<String>) map.get(WemediaConstants.WM_NEWS_TYPE_IMAGE);
        // 封面图片
        String images = news.getImages();
        List<String> list = StrUtil.split(images, ",");
        if (CollectionUtil.isNotEmpty(list)) {
            imageUrl.addAll(list);
        }

        // 获取图片中的文字，进行自定义敏感词库校验
        for (String url : imageUrl) {
            if (StrUtil.isNotEmpty(url)) {
                String imageOCR = AliyunOCR.imageOCR(getFileStream(url));
                text += imageOCR;
            }
        }

        // 校验前查看text
        System.out.println("text = " + text);

        // 先进行本地的DFA校验（自定义敏感词）
        if (handleSensitiveScan(text)) {
            news.setStatus(WmNews.Status.FAIL.getCode());
            news.setReason(NewsConstants.WM_NEWS_FAIL);
            updateById(news);
            return;
        }

        // 文章审核
        String checkContent = AliyunDetect.checkContent(text);
        // 返回为空 说明阿里云服务调用失败 可以使用重试机制进行多次调用（只能解决网络问题）
        // 可以使用定时任务去数据库中扫描 status=1 再进行审核
        if (StrUtil.isBlank(checkContent)) {
            // 可以先做记录阿里云异常信息，在return
            System.out.println("文本校验异常~");
            return;
        }

        // 处理审核结果
        for (String url : imageUrl) {
            // 图片审核
            String checkImage;
            if (StrUtil.isNotBlank(url)) {
                checkImage = AliyunDetect.imageFileDetect(getFileStream(url));
            } else {
                // 无图模式
                checkImage = "pass";
            }
            if (StrUtil.isBlank(checkImage)) {
                // 可以先做记录阿里云异常信息，在return
                System.out.println("图片校验异常~");
                return;
            }
            if (StrUtil.equals(AliyunConstants.ALIYUN_DETECT_PASS, checkContent, true) && StrUtil.equals(AliyunConstants.ALIYUN_DETECT_PASS, checkImage, true)) {
                news.setStatus(WmNews.Status.SUCCESS.getCode());
                news.setReason(NewsConstants.WM_NEWS_SUCCESS);
            } else if (StrUtil.equals(AliyunConstants.ALIYUN_DETECT_BLOCK, checkContent, true) || StrUtil.equals(AliyunConstants.ALIYUN_DETECT_BLOCK, checkImage, true)) {
                news.setStatus(WmNews.Status.FAIL.getCode());
                news.setReason(NewsConstants.WM_NEWS_FAIL);
            } else {
                news.setStatus(WmNews.Status.ADMIN_AUTH.getCode());
                news.setReason(NewsConstants.WM_NEWS_MANUAL);
            }
        }

        // 更新文章的审核状态
        updateById(news);
    }

    /**
     * 得到本地服务器文件的文件流
     *
     * @param url 本地服务器中图片URL地址
     * @return 字节数组
     */
    private byte[] getFileStream(String url) {
        try {
            URL httpUrl = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) httpUrl.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5 * 1000);
            //通过输入流获取图片数据
            InputStream inStream = conn.getInputStream();
            //得到图片的二进制数据
            return readInputStream(inStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 从文件输入流中获取数据
     *
     * @param is 文件输入流
     * @return 字节数组
     * @throws Exception Exception
     */
    private byte[] readInputStream(InputStream is) throws Exception {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = is.read(buffer)) != -1) {
            outStream.write(buffer, 0, len);
        }
        is.close();
        return outStream.toByteArray();
    }

    /**
     * 文章的自动发布
     *
     * @param news 文章
     */
    @Override
    // @Async
    public void publishNewsToArticle(WmNews news) {
        // 审核通过后，我们要进行文章的自动发布
        // 拼接数据 用于文章数据的保存
        ArticleDto articleDto = new ArticleDto();
        BeanUtil.copyProperties(news, articleDto, "id");
        WmUser user = userService.getOne(Wrappers.lambdaQuery(WmUser.class).eq(WmUser::getId, news.getUserId()));
        articleDto.setAuthorId(user.getApAuthorId());
        articleDto.setAuthorName(user.getName());
        WmChannel channel = channelService.getById(articleDto.getChannelId());
        articleDto.setChannelName(channel.getName());
        // 封面布局
        if (news.getType().equals(WemediaConstants.WM_NEWS_NONE_IMAGE)) {
            articleDto.setLayout(0);
        } else if (news.getType().equals(WemediaConstants.WM_NEWS_SINGLE_IMAGE)) {
            articleDto.setLayout(1);
        } else if (news.getType().equals(WemediaConstants.WM_NEWS_MANY_IMAGE)) {
            articleDto.setLayout(2);
        }
        // 文章标记 - 默认标记为普通文章
        articleDto.setFlag(0);

        // 设置 点赞 收藏 评论 阅读 数量默认为0
        articleDto.setLikes(0);
        articleDto.setCollection(0);
        articleDto.setComment(0);
        articleDto.setViews(0);

        // todo: 失败-远程调用接口实现文章的自动发布
        ResponseResult responseResult = articleFeignClient.saveArticle(articleDto);
        // 发布成功 返回文章id
        if (ObjectUtil.isNotEmpty(responseResult.getData())) {
            // 更新文章id
            news.setArticleId(Convert.toLong(responseResult.getData()));
            // 更新文章状态
            news.setStatus(NewsConstants.WM_NEWS_STATUS_RELRASE);
            news.setReason("审核通过，已成功发布");
            updateById(news);
        }
    }

    /**
     * 自定义敏感词检测
     *
     * @param content 检测文本
     * @return 是否包含敏感词
     */
    public Boolean handleSensitiveScan(String content) {
        // 获取敏感词
        List<WmSensitive> sensitiveList = sensitiveService.list();
        ArrayList<String> list = new ArrayList<>();
        for (WmSensitive sensitive : sensitiveList) {
            list.add(sensitive.getSensitives());
        }

        // 初始化敏感词库
        SensitiveWordUtil.initMap(list);

        // 判断文章中是否包含敏感词
        Map<String, Integer> map = SensitiveWordUtil.matchWords(content);

        return map.size() > 0;
    }
}
